/**
 * @file fs_fatfs.c
 * @author LokLiang (lokliang@163.com)
 * @brief 把 FatFs 模块关联到 fs.h 的代码
 * @version 0.1
 * @date 2023-06-12
 *
 * @copyright Copyright (c) 2023
 *
 */

#include "components/fs.h"
#include "ff.h"
#include "fs_init_fatfs.h"

#include "sys_init.h"

#include <stdlib.h>
#include <string.h>
#include <errno.h>

static int _translate_error(FRESULT res);
static const TCHAR *_translate_volume_name(const char *volume_name);

static int _fatfs_open(fs_file_t *fp, fs_path_t *path, uint8_t flags);
static int _fatfs_close(fs_file_t *fp);
static int _fatfs_read(fs_file_t *fp, void *dest, size_t size);
static int _fatfs_write(fs_file_t *fp, const void *src, size_t size);
static int _fatfs_lseek(fs_file_t *fp, int offset, uint8_t whence);
static int _fatfs_truncate(fs_file_t *fp, int length);
static int _fatfs_sync(fs_file_t *fp);
static int _fatfs_tell(fs_file_t *fp);
static int _fatfs_opendir(fs_dir_t *dp, fs_path_t *path);
static int _fatfs_closedir(fs_dir_t *dp);
static int _fatfs_readdir(fs_dir_t *dp, fs_file_info_t *info);
static int _fatfs_getcwd(fs_mount_t *mp, char *path, int length);
static int _fatfs_chdir(fs_mount_t *mp, fs_path_t *path);
static int _fatfs_mkdir(fs_mount_t *mp, fs_path_t *path);
static int _fatfs_unlink(fs_mount_t *mp, fs_path_t *path);
static int _fatfs_rename(fs_mount_t *mp, fs_path_t *from, fs_path_t *to);
static int _fatfs_stat(fs_mount_t *mp, fs_path_t *path, fs_file_info_t *info);
static int _fatfs_statvfs(fs_mount_t *mp, fs_statvfs_t *stat, const char *volume_name);
static int _fatfs_mkfs(fs_mount_t *mp, const char *volume_name);
static int _fatfs_mount(fs_mount_t *mp, const char *volume_name);
static int _fatfs_unmount(fs_mount_t *mp, const char *volume_name);

static fs_api_t const s_fatfs_api = {
    .open = _fatfs_open,
    .close = _fatfs_close,
    .read = _fatfs_read,
    .write = _fatfs_write,
    .lseek = _fatfs_lseek,
    .truncate = _fatfs_truncate,
    .sync = _fatfs_sync,
    .tell = _fatfs_tell,
    .opendir = _fatfs_opendir,
    .closedir = _fatfs_closedir,
    .readdir = _fatfs_readdir,
    .getcwd = _fatfs_getcwd,
    .chdir = _fatfs_chdir,
    .mkdir = _fatfs_mkdir,
    .unlink = _fatfs_unlink,
    .rename = _fatfs_rename,
    .stat = _fatfs_stat,
    .statvfs = _fatfs_statvfs,
    .mkfs = _fatfs_mkfs,
    .mount = _fatfs_mount,
    .unmount = _fatfs_unmount,
};

static int _translate_error(FRESULT res)
{
    switch (res)
    {
    case FR_OK:
        return 0;
    case FR_NO_FILE:
    case FR_NO_PATH:
    case FR_INVALID_NAME:
        return -ENOENT;
    case FR_DENIED:
        return -EACCES;
    case FR_EXIST:
        return -EEXIST;
    case FR_INVALID_OBJECT:
        return -EBADF;
    case FR_WRITE_PROTECTED:
        return -EROFS;
    case FR_INVALID_DRIVE:
    case FR_NOT_ENABLED:
    case FR_NO_FILESYSTEM:
        return -ENODEV;
    case FR_NOT_ENOUGH_CORE:
        return -ENOMEM;
    case FR_TOO_MANY_OPEN_FILES:
        return -EMFILE;
    case FR_INVALID_PARAMETER:
        return -EINVAL;
    case FR_LOCKED:
    case FR_TIMEOUT:
    case FR_MKFS_ABORTED:
    case FR_DISK_ERR:
    case FR_INT_ERR:
    case FR_NOT_READY:
        return -EIO;
    }

    return -EIO;
}

static const TCHAR *_translate_volume_name(const char *volume_name)
{
    int id = f_disk_get_id_by_name(volume_name);
    if (id < 0)
    {
        return NULL;
    }
    return f_disk_get_volume(id);
}

static int _fatfs_open(fs_file_t *fp, fs_path_t *path, uint8_t flags)
{
    fp->file_hdl = calloc(1, sizeof(FIL));
    if (fp->file_hdl == NULL)
    {
        return -ENOMEM;
    }

#if !defined(CONFIG_FS_FATFS_READ_ONLY)
#else /* #if !defined(CONFIG_FS_FATFS_READ_ONLY) */
    flags &= ~FS_O_WRITE;
    flags &= ~FS_O_CREATE;
    flags &= ~FS_O_APPEND;
#endif

    BYTE mode = 0;
    mode |= (flags & FS_O_READ) ? FA_READ : 0;
    mode |= (flags & FS_O_WRITE) ? FA_WRITE : 0;
    mode |= (flags & FS_O_CREATE) ? FA_OPEN_ALWAYS : 0;
    mode |= (flags & FS_O_APPEND) ? FA_OPEN_APPEND : 0;

    FRESULT res = f_open(fp->file_hdl, path, mode);
    if (res == FR_OK)
    {
        fp->flags = flags;
        return 0;
    }
    else
    {
        free(fp->file_hdl);
        fp->file_hdl = NULL;
        return _translate_error(res);
    }
}

static int _fatfs_close(fs_file_t *fp)
{
    FRESULT res = f_close(fp->file_hdl);
    free(fp->file_hdl);
    fp->file_hdl = NULL;
    return _translate_error(res);
}

static int _fatfs_read(fs_file_t *fp, void *dest, size_t size)
{
    UINT br;
    FRESULT res = f_read(fp->file_hdl, dest, size, &br);
    if (res == FR_OK)
    {
        return br;
    }
    else
    {
        return _translate_error(res);
    }
}

static int _fatfs_write(fs_file_t *fp, const void *src, size_t size)
{
#if !defined(CONFIG_FS_FATFS_READ_ONLY)
    UINT bw;
    int res = -ENOTSUP;

    if (fp->flags & FS_O_APPEND)
    {
        res = f_lseek(fp->file_hdl, f_size((FIL *)fp->file_hdl));
        if (res != FR_OK)
        {
            return _translate_error(res);
        }
    }

    res = f_write(fp->file_hdl, src, size, &bw);
    if (res == FR_OK)
    {
        return bw;
    }
    else
    {
        return _translate_error(res);
    }
#else /* #if !defined(CONFIG_FS_FATFS_READ_ONLY) */
    return -ENOTSUP;
#endif
}

static int _fatfs_lseek(fs_file_t *fp, int offset, uint8_t whence)
{
    FRESULT res = FR_OK;
    FSIZE_t fsize = f_size((FIL *)fp->file_hdl);
    int ofs;

    switch (whence)
    {
    case FS_SEEK_SET:
        ofs = offset;
        break;
    case FS_SEEK_CUR:
        ofs = f_tell((FIL *)fp->file_hdl) + offset;
        break;
    case FS_SEEK_END:
        ofs = fsize + offset;
        break;
    default:
        return -EINVAL;
    }

    if ((ofs < 0) || (ofs > fsize))
    {
        return -EINVAL;
    }

    res = f_lseek(fp->file_hdl, ofs);
    if (res != FR_OK)
    {
        return _translate_error(res);
    }

    uint8_t data;
    UINT br;
    f_read(fp->file_hdl, &data, 1, &br);
    f_lseek(fp->file_hdl, ofs);
    return ((FIL *)fp->file_hdl)->sect;
}

static int _fatfs_truncate(fs_file_t *fp, int length)
{
#if !defined(CONFIG_FS_FATFS_READ_ONLY)
    FSIZE_t fsize = f_size((FIL *)fp->file_hdl);

    FRESULT res = f_lseek(fp->file_hdl, length);
    if (res != FR_OK)
    {
        return _translate_error(res);
    }

    if (length < fsize)
    {
        res = f_truncate(fp->file_hdl);
    }
    else
    {
        length = f_tell((FIL *)fp->file_hdl); // 有可能容量不足，需要获取扩展后的真实长度

        res = f_lseek(fp->file_hdl, fsize);
        if (res != FR_OK)
        {
            return _translate_error(res);
        }

        UINT bw;
        uint8_t c = 0;
        for (int i = fsize; i < length; i++)
        {
            res = f_write(fp->file_hdl, &c, 1, &bw);
            if (res != FR_OK)
            {
                break;
            }
        }
    }

    return _translate_error(res);
#else /* #if !defined(CONFIG_FS_FATFS_READ_ONLY) */
    return -ENOTSUP;
#endif
}

static int _fatfs_sync(fs_file_t *fp)
{
#if !defined(CONFIG_FS_FATFS_READ_ONLY)
    return _translate_error(f_sync(fp->file_hdl));
#else /* #if !defined(CONFIG_FS_FATFS_READ_ONLY) */
    return -ENOTSUP;
#endif
}

static int _fatfs_tell(fs_file_t *fp)
{
    return f_tell((FIL *)fp->file_hdl);
}

static int _fatfs_opendir(fs_dir_t *dp, fs_path_t *path)
{
    dp->dir_hdl = calloc(1, sizeof(FIL));
    if (dp->dir_hdl == NULL)
    {
        return -ENOMEM;
    }

    FRESULT res = f_opendir(dp->dir_hdl, path);
    if (res == FR_OK)
    {
        return 0;
    }
    else
    {
        free(dp->dir_hdl);
        dp->dir_hdl = NULL;
        return _translate_error(res);
    }
}

static int _fatfs_closedir(fs_dir_t *dp)
{
    FRESULT res = f_closedir(dp->dir_hdl);
    free(dp->dir_hdl);
    dp->dir_hdl = NULL;
    return _translate_error(res);
}

static int _fatfs_readdir(fs_dir_t *dp, fs_file_info_t *info)
{
    FRESULT res;
    FILINFO fno;

    res = f_readdir(dp->dir_hdl, &fno);
    if (res == FR_OK)
    {
        strncpy(info->name, fno.fname, FS_FILE_NAME_LEN);
        if (fno.fname[0] != '\0')
        {
            info->type = ((fno.fattrib & AM_DIR) ? FS_INFO_TYPE_DIR : FS_INFO_TYPE_FILE);
            info->fsize = fno.fsize;
        }
    }

    return _translate_error(res);
}

static int _fatfs_getcwd(fs_mount_t *mp, char *path, int length)
{
    return _translate_error(f_getcwd(path, length));
}

static int _fatfs_chdir(fs_mount_t *mp, fs_path_t *path)
{
    f_chdrive(path);
    return _translate_error(f_chdir(path));
}

static int _fatfs_mkdir(fs_mount_t *mp, fs_path_t *path)
{
#if !defined(CONFIG_FS_FATFS_READ_ONLY)
    return _translate_error(f_mkdir(path));
#else /* #if !defined(CONFIG_FS_FATFS_READ_ONLY) */
    return -ENOTSUP;
#endif
}

static int _fatfs_unlink(fs_mount_t *mp, fs_path_t *path)
{
#if !defined(CONFIG_FS_FATFS_READ_ONLY)
    return _translate_error(f_unlink(path));
#else /* #if !defined(CONFIG_FS_FATFS_READ_ONLY) */
    return -ENOTSUP;
#endif
}

static int _fatfs_rename(fs_mount_t *mp, fs_path_t *from, fs_path_t *to)
{
#if !defined(CONFIG_FS_FATFS_READ_ONLY)
    FILINFO fno;
    FRESULT res = f_stat(to, &fno);
    if (FR_OK == res)
    {
        res = f_unlink(to);
        if (FR_OK != res)
        {
            return _translate_error(res);
        }
    }
    res = f_rename(from, to);
    return _translate_error(res);
#else /* #if !defined(CONFIG_FS_FATFS_READ_ONLY) */
    return -ENOTSUP;
#endif
}

static int _fatfs_stat(fs_mount_t *mp, fs_path_t *path, fs_file_info_t *info)
{
    FILINFO fno;
    FRESULT res = f_stat(path, &fno);
    if (res == FR_OK && fno.fname[0] != '\0')
    {
        strncpy(info->name, fno.fname, FS_FILE_NAME_LEN);
        if (fno.fname[0] != '\0')
        {
            info->type = ((fno.fattrib & AM_DIR) ? FS_INFO_TYPE_DIR : FS_INFO_TYPE_FILE);
            info->fsize = fno.fsize;
        }
    }
    return _translate_error(res);
}

static int _fatfs_statvfs(fs_mount_t *mp, fs_statvfs_t *stat, const char *volume_name)
{
    FATFS *fs;
    DWORD f_bfree = 0;

    FRESULT res = f_getfree(_translate_volume_name(volume_name), &f_bfree, &fs);
    if (res != FR_OK)
    {
        return _translate_error(res);
    }

#if FF_MAX_SS != FF_MIN_SS
    stat->f_bsize = fs->ssize;
#else
    stat->f_bsize = FF_MIN_SS;
#endif
    stat->f_frsize = fs->csize * stat->f_bsize;
    stat->f_blocks = (fs->n_fatent - 2);
    stat->f_bfree = f_bfree;

    return 0;
}

static int _fatfs_mkfs(fs_mount_t *mp, const char *volume_name)
{
    void *work = malloc(FF_MAX_SS);
    FRESULT res = f_mkfs(_translate_volume_name(volume_name), 0, work, FF_MAX_SS);
    free(work);
    return _translate_error(res);
}

static int _fatfs_mount(fs_mount_t *mp, const char *volume_name)
{
    int id = f_disk_get_id_by_name(volume_name);
    if (id < 0)
    {
        /* 整理驱动器名为 disk:/ff:volume_name */
        char name[10 + CONFIG_MAX_VOL_NAME_LEN] = "disk:/ff:";
        strncat(name, volume_name, sizeof(name) - 1);

        /* 绑定驱动器名对应的对象 */
        device_disk_fatfs_t dev = device_disk_fatfs_binding(name);
        if (dev == NULL)
        {
            // SYS_LOG_ERR("Failure: device '%s' not found\r\n", volume_name);
            return -ENODEV;
        }

        if (f_disk_get_id(dev) >= 0)
        {
            // SYS_LOG_ERR("Failure: device '%s' already mounted\r\n", volume_name);
            return -EBUSY;
        }

        /* 注册驱动器 */
        if (f_disk_regist(dev, volume_name, -1) != 0)
        {
            // SYS_LOG_ERR("%s fs init ERROR!\r\n", volume_name);
            return -EINVAL;
        }
    }

    mp->fs_hdl = calloc(1, sizeof(FATFS));
    if (mp->fs_hdl == NULL)
    {
        return -ENOMEM;
    }

    const TCHAR *path = _translate_volume_name(volume_name);
    FRESULT res = f_mount(mp->fs_hdl, path, 1);
    if (res == FR_NO_FILESYSTEM)
    {
        BYTE work[FF_MAX_SS];
        res = f_mkfs(path, 0, work, sizeof(work));
        res = f_mount(NULL, path, 0);
        res = f_mount(mp->fs_hdl, path, 1);
    }
    if (res != FR_OK)
    {
        _fatfs_unmount(mp, volume_name);
        return -1;
    }

    return _translate_error(res);
}

static int _fatfs_unmount(fs_mount_t *mp, const char *volume_name)
{
    FRESULT res = f_mount(NULL, _translate_volume_name(volume_name), 0);
    if (res == FR_OK)
    {
        if (mp->fs_hdl)
        {
            free(mp->fs_hdl);
            mp->fs_hdl = NULL;
        }

        f_disk_unregist(volume_name);
    }

    return _translate_error(res);
}

static int _fs_register(void)
{
    return fs_regist("/ff", &s_fatfs_api);
}
INIT_EXPORT_COMPONENT(_fs_register);
